//==============================================================================
//
//          DHTTPClient - the HTTP client class in the ofc-library
//
//               Copyright (C) 2004  Dick van Oudheusden
//  
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
//==============================================================================
// 
//  $Date: 2005-02-05 06:25:37 $ $Revision: 1.11 $
//
//==============================================================================

#include "ofc/DHTTPClient.h"

//
// ToDo:
// 1. Chunked transfer
// 2. POST/PUT
// 3. Cookies
//
 
#if _INTERFACE_

#include "ofc/config.h"

#include "ofc/DURL.h"
#include "ofc/DSocket.h"
#include "ofc/DHashTable.h"


#define DHTTP_PORT     (18)

#define DHC_UNKNOWN    (0)
#define DHC_HEAD       (1)
#define DHC_GET        (2)

//
// The DHTTPClient class implements a HTTP client. The class does not support
// the POST and PUT requests. Also the responses with a chunked transfer 
// encoding are not handled.
//

@interface DHTTPClient : Object
{
@private
  DSocket            *_client;       // the client socket
  BOOL                _shouldOpen;   // should the socket be opened?
  int                 _state;        // the state of the client

  DText              *_server;       // the server host name (or proxy)
  DInetSocketAddress *_address;      // the address of the server (or proxy)

  int                 _type;         // the request type (DC_HEAD, DHC_GET, ..)
  int                 _major;        // the major version number (def. 1)
  int                 _minor;        // the minor version number (def. 1)
  BOOL                _shouldClose;  // should the connection close after comm.?
  DText               *_request;     // the request

  DData               *_response;    // the respone
  int                  _respIndex;   // the index in the response string
  DText               *_line;        // one line of the response
  int                  _reason;      // the received reply reason
  DText               *_reasonText;  // the received reason text
  DHashTable          *_headers;     // the received headers
  DData               *_body;        // the received body in data
  BOOL                 _useChunked;  // is chunked used ?
  int                  _length;      // the length of the body
  int                  _peerMajor;   // the major version number of the peer
  int                  _peerMinor;   // the minor version number of the peer
}

#endif

#if _PRIVATE_

#define DHC_IDLE          (0)
#define DHC_REQUESTING    (1)
#define DHC_WAIT_RESPONSE (2)
#define DHC_RECEIVED      (3)

static char *_requests[] =
{
  "????",
  "HEAD",
  "GET",
};

#endif


@implementation DHTTPClient


//// Constructors

//
// Initialise the HTTP Client
//
// @return the object
//
- (DHTTPClient *) init
{
  [super init];

  _client      = [DSocket new];
  _shouldOpen  = YES;
  _state       = DHC_IDLE;

  _server      = [DText new];
  _address     = [DInetSocketAddress new];
  _major       = 1;
  _minor       = 1;
  _type        = DHC_UNKNOWN;
  _shouldClose = NO;
  _request     = [DText new];

  _response    = nil;
  _respIndex   = 0;
  _line        = [DText new];
  _reason      = 0;
  _reasonText  = [DText new];
  _headers     = nil;
  _body        = [DData new];
  _useChunked  = NO;
  _length      = 0;
  _peerMajor   = 0;
  _peerMinor   = 0;
  
  return self;
}

//
// Initialise the HTTP Client with a client version
//
// @param major    the major version number
// @param minor    the minor version number
// 
// @return the object
//
- (DHTTPClient *) init :(int) major :(int) minor
{
  [self init];
  
  _major = major;
  _minor = minor;
  
  return self;
}

//// Deconstructor

//
// Free the HTTP client object
//
// @return the object
//
- free
{
  [_client  free];
  [_request free];
  
  [_server  free];
  [_address free];
  
  [_body free];

  if (_headers != nil)
  {
    [_headers free];
  }
  
  if (_response != nil)
  {
    [_response free];
  }
  
  [_line       free];
  [_reasonText free];
  
  return [super free];
}


//// Copy related methods

//
// Deepen the object (not implemented)
//
//
- deepen
{
  WARNING(DW_METHOD_NOT_IMPL, "deepen");
  
  return self;
}


//// Member methods

//
// Return the local client object
// 
// @return a reference to the client object
//
- (DSocket *) socket
{
  return _client;
}


//// Sending methods

// 
// Send the start of the request
//
// @param request      the request (DHC_HEAD, DHC_GET ..) 
// @param path         the (absolute) url to get
// 
// @return success
// 
- (BOOL) sendStartRequest :(int) request :(DURL *) path
{
  return [self sendStartRequest :request :path :NULL :0];
}

// 
// Send the start of the request via a proxy
//
// @param request      the request (DHTTP_HEAD, DHTTP_GET, ..)
// @param path         the (absolute) url to get
// @param proxy        the host name of the proxy (or NULL for no proxy)
// @param proxyPort    the proxy port number
// 
// @return success
// 
- (BOOL) sendStartRequest :(int) request :(DURL *) path :(const char *) proxy :(int) proxyPort
{
  BOOL ok = NO;

  if ((_state != DHC_IDLE) && (_state != DHC_RECEIVED))
  {
    WARNING(DW_INVALID_STATE, "idle");
  }
  else if ((request < DHC_HEAD) || (request > DHC_GET))
  {
    WARNING(DW_INVALID_ARG, "request");
  }
  else if (path == nil)
  {
    WARNING(DW_NIL_NOT_ALLOWED, "path");
  }
  else if ([path host] == NULL)
  {
    WARNING(DW_INVALID_ARG, "path");
  }
  else if ((proxy != NULL) && (proxyPort <= 0))
  {
    WARNING(DW_INVALID_ARG, "proxyPort");
  }
  else
  {
    _type = request;
    
    if (proxy != NULL)
    {
      [_request format :"%s http://%s%s HTTP/%d.%d\r\n", _requests[_type], [path host], [path path], _major, _minor];
    }
    else
    {
      [_request format :"%s %s HTTP/%d.%d\r\n", _requests[_type], [path path], _major, _minor];
    }

    ok = [self _sendFirstRequest :path :proxy :proxyPort :[_request cstring]];
    
    if (ok)
    {
      _state = DHC_REQUESTING;
      
      if (((_major == 1) && (_minor >= 1)) || (_major > 1))
      {
        ok = [self sendHeader :"Host" :[_server  cstring]];
      }
    }
  }
  
  return ok;
}

//
// Send a (general) header after the request
// 
// @param header       the header to send
// @param argument     the argument for the header
// 
// @return success
// 
- (BOOL) sendHeader :(const char *) header :(const char *) argument
{
  BOOL ok = NO;

  if (_state != DHC_REQUESTING)
  {
    WARNING(DW_INVALID_STATE, "requesting");
  }
  else if ((header == NULL) || (*header == EOS))
  {
    WARNING(DW_INVALID_ARG, "header");
  }
  else if ((argument == NULL) || (*argument == EOS))
  {
    WARNING(DW_INVALID_ARG, "argument");
  }
  else
  {
    [_request set    :header];
    [_request append :": "];
    [_request append :argument];
    [_request append :"\r\n"];

    ok = ([_client send :[_request cstring] :0] > 0);
  }
  
  return ok;
}

//
// Send Accept-Language header
// 
// @param languages   a string containing the languages (en-us, en)
//
// @return success
// 
- (BOOL) sendAcceptLanguageHeader :(const char *) languages
{
  return [self sendHeader :"Accept-Language" :languages];
}

//
// Send User-Agent header
// 
// @param agent     a c-string with the user agent
// 
// @return success
//
- (BOOL) sendUserAgentHeader :(const char *) agent
{
  return [self sendHeader :"User-Agent" :agent];
}

//
// Send Referer header
// 
// @param referer   a c-string with the referer link
// 
// @return success
// 
- (BOOL) sendRefererHeader :(const char *) referer
{
  return [self sendHeader :"Referer" :referer];
}

//
// Send Connection header
// 
// @param connection   a c-string with "close" or "keep-alive"
// 
// @return success
// 
- (BOOL) sendConnectionHeader :(const char *) connection
{
  return [self sendHeader :"Connection" :connection];
}

//
// Send the end of the request to the server/proxy
// 
// @return success
// 
- (BOOL) sendEndRequest
{
  BOOL ok = NO;
  
  if (_state != DHC_REQUESTING)
  {
    WARNING(DW_INVALID_STATE, "requesting");
  }
  else
  {
    ok = ([_client send :"\r\n" :0] > 0); // add end of request
  }
  
  if (ok)
  {
    _state = DHC_WAIT_RESPONSE;
  }
  
  return ok;
}


//// Receiving methods

//
// Receive a reply from the server
// 
// @return success
// 
- (BOOL) receiveReply
{
  _reason = -1;

  if (_state != DHC_WAIT_RESPONSE)
  {
    WARNING(DW_INVALID_STATE, "wait_response");
  }
  else
  {
    [self _startProcessingResponse];
    
    [self _processFirstLine];
    
    if (_reason >= 0)
    {
      [self _processHeaders];
    }
    
    if (_reason >= 0)
    {
      if  (!_useChunked)
      {
        if (_length > 0)
        {
          [self _processNonChunkedData];
        }
      }
      else if ((_reason >= 200) && (_reason < 299))
      {
        WARNING(DW_UNKNOWN_WARNING, "Chunked transfer not (yet) supported");
        _reason = -1;
      }
    }
  }

  if ((_reason < 0) || (_shouldClose))
  {
    [_client close]; 
    
    _shouldOpen = YES;
  }
  
  if (_response != nil)
  {
    [_response free]; _response = nil;
  }
    
  if (_reason >= 0)
  {
    _state = DHC_RECEIVED;
  }
  else
  {
    [_reasonText set :"Invalid HTTP Response"];
    
    _state = DHC_IDLE;
  }

  return (_reason >= 0);
}

//
// Return the major version number of the peer (after receiveReply)
// 
// @return the major version number
//
- (int) peerMajor
{
  if (_state != DHC_RECEIVED)
  {
    WARNING(DW_INVALID_STATE, "received");
    
    return -1;
  }
  
  return _peerMajor;
}

//
// Return the minor version number of the peer (after receiveReply)
// 
// @return the minor version number
//
- (int) peerMinor
{
  if (_state != DHC_RECEIVED)
  {
    WARNING(DW_INVALID_STATE, "received");
    
    return -1;
  }
  
  return _peerMinor;
}

//
// Get the reason (after receiveReply)
// 
// @return the reason code (or -1)
// 
- (int) reason
{
  if (_state != DHC_RECEIVED)
  {
    WARNING(DW_INVALID_STATE, "received");
    
    return -1;
  }
  
  return _reason;
}

//
// Get the reason text (after receiveReply)
// 
// @return the reason text (or NULL for error)
// 
- (const char *) reasonText
{
  if (_state != DHC_RECEIVED)
  {
    WARNING(DW_INVALID_STATE, "received");
    
    return NULL;
  }
  
  return [_reasonText cstring];
}
  
//
// Get the argument of a header (after receiveReply)
// 
// @param header   the header (all lower case)
// 
// @return a (new) text object with the argument (or nil)
// 
- (DText *) header :(const char *) header
{
  DText *val = nil;
  
  if (_state != DHC_RECEIVED)
  {
    WARNING(DW_INVALID_STATE, "received");
  }
  else if (_headers != nil)
  {
    DText *key = [DText new];
    
    [key set :header];
    
    val = [_headers get :key];
    
    [key free];
  }
  
  return (val != nil) ? [val copy] : nil;
}

//
// Get all received headers (after receiveReply)
// 
// @return a (new) iterator on the hash table with all the received headers (or nil)
// 
- (DHashIterator *) headers
{
  DHashIterator *iter = nil;
  
  if (_state != DHC_RECEIVED)
  {
    WARNING(DW_INVALID_STATE, "received");
  }
  else if (_headers != nil)
  {
    iter = [DHashIterator alloc];
    
    [iter init :_headers];
  }
  
  return iter;
}

//
// Get the body of the received message (after receiveReply)
// 
// @return a received data (or NULL for error)
// 
- (const unsigned char *) body
{
  if (_state != DHC_RECEIVED)
  {
    WARNING(DW_INVALID_STATE, "received");
    
    return NULL;
  }
  
  return [_body data];
}

//
// Get the length of the body of the received message (after receiveReply)
// 
// @return a received length
// 
- (unsigned long) bodyLength
{
  return [_body length];
}


//// Response processing methods (private methods)

//
// Send the first line to the server
// 
// @param path      the path of the server
// @param proxy     the name of the proxy (or NULL)
// @param proxyPort the port of the proxy (or 0)
// @param request   the request for the server
// 
// @return success
//
- (BOOL) _sendFirstRequest :(DURL *) path :(const char *) proxy :(int) proxyPort :(const char *) request
{
  BOOL ok    = NO;
  int  retry = 1; 
  
  if (!_shouldOpen)
  {
    // Check for server or proxy change
    if (proxy != NULL)
    {
      if ([_server icompare :proxy] != 0)
      {
//printf("Shutdown\n");
        _shouldOpen = YES;
      }
    }
    else
    {
      if ([_server icompare :[path host]] != 0)
      {
//printf("Shutdown\n");
        _shouldOpen = YES;
      }
    }
  }

  // try connection with server and transmit
  while ((!ok) && (retry >= 0))
  {
    ok = YES;
    
    if (_shouldOpen)
    {
      if (proxy != NULL)
      {
        [_address host :proxy :proxyPort];
        
        [_server set :proxy];
      }
      else
      {
        int port = [path port];
        
        if (port == 0)
        {
          port = DHTTP_PORT; // use the default port
        }
        
        [_address host :[path host] :port];
        
        [_server set :[path host]];
      }

      // open method will auto close the connection if still open
      ok  = [_client open :DSA_AF_INET :DSK_STREAM :[DSocket protocol :"tcp"]];

      if (ok)
      {
        ok = [_client connect :_address];
      }
    
//printf("Connecting(%d) to %s port %d\n", ok, [[_address host] cstring], [_address port]);
      
      if (ok)
      {
        _shouldOpen = NO;
      }
    }
    
    if (ok)
    {
      ok = ([_client send :request :0] > 0);

//printf("Sending(%d):%s", ok, [_request cstring]);
      if (!ok)
      {
        _shouldOpen = YES;
      }
    }
    
    retry--;
  }
  
  return ok;
}

//
// Start processing the response
// 
// @return none
// 
- (void) _startProcessingResponse
{
  // clear the information from last response
  if (_headers != nil)
  {
    [_headers free]; _headers = nil;
  }
      
  _headers = [DHashTable alloc];
      
  [_headers init :[DText class]];
    
  [_body clear];
        
  _reason = -1;
  [_reasonText clear];
  
  _respIndex = 0;
  
  _length      = 0;
  _useChunked  = NO;
  _shouldClose = NO;
  
  _peerMajor = 0;
  _peerMinor = 0;
}

//
// Receive a line from the client
// 
// @return success
// 
- (BOOL) _receiveLine
{
  BOOL ok = YES;
  char ch = EOS;

  [_line clear];
  
  do
  {
    while ((ok) && ((_response == nil) || (_respIndex >= [_response length])))
    {
      _respIndex = 0;
      
      if (_response != nil)
      {
        [_response free]; _response = nil;
      }

      _response = [_client recv :16384 :0];

      if (_response == nil)
      {
        ok = NO;
      }
    }
  
    if (ok)
    {
      ch = [_response get :_respIndex];
      
      if ((ch != '\r') && (ch != '\n'))
      {
        [_line push :ch];
      }
      
      _respIndex++;
    }
  }
  while ((ok) && (ch != '\n'));   

//printf("Received:%s\n", [_line cstring]);

  return ok;
}

//
// Process the first line of the response
// 
// @return none
// 
- (void) _processFirstLine
{
  BOOL ok = YES;
  
  ok = [self _receiveLine];

  // skip empty lines
  while ((ok) && ([[_line strip] length] == 0))
  {
    ok = [self _receiveLine];
  }

  // Scan the line contents
  if (ok)
  {
    ok = [_line imatch :"http/"];
  }
    
  if (ok)
  {
    _peerMajor = [_line scanInt :-1];
      
    if ((_peerMajor == -1) || (![_line cmatch :"."]))
    {
      ok = NO;
    }
  }
      
  if (ok)
  {
    _peerMinor = [_line scanInt :-1];
      
    if ((_peerMinor == -1) || ([_line skipWhiteSpace] == 0))
    {
      ok = NO;
    }
//printf("peer version : %d.%d\n", _peerMajor, _peerMinor);
  }

  // Close connection if version < 1.1
  if ((ok) && ((_peerMajor < 1) || ((_peerMajor == 1) && (_peerMinor == 0))))
  {
    _shouldClose = YES;
  }
  
  if (ok)
  {
    _reason = [_line scanInt :-1];
      
    if ((_reason == -1) || ([_line skipWhiteSpace] == 0))
    {
      ok = NO;
    }
    else
    {
      [_reasonText free]; _reasonText = nil;
      
      _reasonText = [_line readText];
    }
//printf("Reason : %d Text: %s\n", _reason, [_reasonText cstring]);
  }

  if (!ok)
  {
    _reason = -1;
  }
}

//
// Process the headers in the response
// 
// @return none
// 
- (void) _processHeaders
{
  BOOL ok = YES;
  
  ok = [self _receiveLine];
      
  while ((ok) && ([[_line strip] length] > 0))
  {
    DText *header = [_line scanText :':'];
        
    if (header != nil)
    {
      DText *value = nil;
      
      [header lower];
          
      [_line skipWhiteSpace];

      value = [_line readText];
          
      [_headers insert :header :value];
        
//printf("header : %s value : %s\n", [header cstring], [value cstring]);
      
      // Process some headers..
      if ([header ccompare :"content-length"] == 0)
      {
        _length = [value toInt];
      }
      else if ([header ccompare :"connection"] == 0)
      {
        if ([value icompare :"close"] == 0)
        {
          _shouldClose = YES;
        }
        else if ([value icompare :"keep-alive"] == 0)
        {
          _shouldClose = NO;
        }
      }
      else if ([header icompare :"transfer-encoding"] == 0)
      {
        if ([value icompare :"chunked"] == 0)
        {
          _useChunked = YES;
        }
      }
      
      [header free];
          
      ok = [self _receiveLine];
    }
    else
    {
      ok = NO;
    }
  }

  if (!ok)
  {
    _reason = -1;
  }

  // Clear the length if the body is empty
  if ((_type == DHC_HEAD) || 
      (_reason == 204)    || 
      (_reason == 304)    ||
      ((_reason >= 100)   &&
       (_reason <  200)))
  {
    _length = 0;
  }
  
}

//
// Process non-chunked data in the response
// 
// @return none
// 
- (void) _processNonChunkedData
{
  int remaining = [_response length] - _respIndex;

//printf("Body length: %d\n", _length);

  if (remaining > 0)
  {
    [_body set :[_response data] + _respIndex :remaining];
        
    _length -= remaining;
  }
  
  [_response free]; _response = nil;
      
  if (_length > 0)
  {
    _response = [_client recv :16384 :0];
  }
      
  while ((_response != nil) && (_length > 0))
  {
    [_body append :[_response data] :[_response length]];
        
    _length -= [_response length];
          
    [_response free]; _response = nil;
        
    if (_length > 0)
    {
      _response = [_client recv :16384 :0];
    }
  }

  if (_length > 0)
  {
    _reason = -1;
  }
}

@end

/*===========================================================================*/
